Исследование объявлений о продаже квартир¶

Данные сервиса Яндекс.Недвижимость — архив объявлений о продаже квартир в Санкт-Петербурге и соседних населённых пунктов за несколько лет. Нужно научиться определять рыночную стоимость объектов недвижимости. Ваша задача — установить параметры. Это позволит построить автоматизированную систему: она отследит аномалии и мошенническую деятельность.

По каждой квартире на продажу доступны два вида данных. Первые вписаны пользователем, вторые — получены автоматически на основе картографических данных. Например, расстояние до центра, аэропорта, ближайшего парка и водоёма.

1. Изучение входной информации¶

Описание данных

  • airports_nearest — расстояние до ближайшего аэропорта в метрах (м)
  • balcony — число балконов
  • ceiling_height — высота потолков (м)
  • cityCenters_nearest — расстояние до центра города (м)
  • days_exposition — сколько дней было размещено объявление (от публикации до снятия)
  • first_day_exposition — дата публикации
  • floor — этаж
  • floors_total — всего этажей в доме
  • is_apartment — апартаменты (булев тип)
  • kitchen_area — площадь кухни в квадратных метрах (м²)
  • last_price — цена на момент снятия с публикации
  • living_area — жилая площадь в квадратных метрах(м²)
  • locality_name — название населённого пункта
  • open_plan — свободная планировка (булев тип)
  • parks_around3000 — число парков в радиусе 3 км
  • parks_nearest — расстояние до ближайшего парка (м)
  • ponds_around3000 — число водоёмов в радиусе 3 км
  • ponds_nearest — расстояние до ближайшего водоёма (м)
  • rooms — число комнат
  • studio — квартира-студия (булев тип)
  • total_area — площадь квартиры в квадратных метрах (м²)
  • total_images — число фотографий квартиры в объявлении

[Описание данных после предобработки и добавления данных](#describe_data_after_preproc)¶

Изучение общей информации о предоставляемых данных¶

In [1]:
import os
import sys
from random import randint
import urllib
import numpy as np
import pandas as pd
from pathlib import Path
import plotly.express as px
import matplotlib.pyplot as plt
In [2]:
path = 'data/real_estate_data.csv'
url = 'https://code.s3.yandex.net/datasets/real_estate_data.csv'
Path('data').mkdir(parents=True, exist_ok=True)
if not os.path.exists(path):
    print(f'real_estate_data.csv не найден. Будет загружен из сети.')
    _ = urllib.request.urlretrieve(url, path)
base = pd.read_csv('data/real_estate_data.csv', sep='\t')
In [3]:
# pd.options.mode.chained_assignment = None 
In [4]:
data = pd.DataFrame(base)
data.info()
print(data.shape)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 23699 entries, 0 to 23698
Data columns (total 22 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   total_images          23699 non-null  int64  
 1   last_price            23699 non-null  float64
 2   total_area            23699 non-null  float64
 3   first_day_exposition  23699 non-null  object 
 4   rooms                 23699 non-null  int64  
 5   ceiling_height        14504 non-null  float64
 6   floors_total          23613 non-null  float64
 7   living_area           21796 non-null  float64
 8   floor                 23699 non-null  int64  
 9   is_apartment          2775 non-null   object 
 10  studio                23699 non-null  bool   
 11  open_plan             23699 non-null  bool   
 12  kitchen_area          21421 non-null  float64
 13  balcony               12180 non-null  float64
 14  locality_name         23650 non-null  object 
 15  airports_nearest      18157 non-null  float64
 16  cityCenters_nearest   18180 non-null  float64
 17  parks_around3000      18181 non-null  float64
 18  parks_nearest         8079 non-null   float64
 19  ponds_around3000      18181 non-null  float64
 20  ponds_nearest         9110 non-null   float64
 21  days_exposition       20518 non-null  float64
dtypes: bool(2), float64(14), int64(3), object(3)
memory usage: 3.7+ MB
(23699, 22)
In [5]:
display(data.head(3))
display(data.describe().T)
total_images last_price total_area first_day_exposition rooms ceiling_height floors_total living_area floor is_apartment ... kitchen_area balcony locality_name airports_nearest cityCenters_nearest parks_around3000 parks_nearest ponds_around3000 ponds_nearest days_exposition
0 20 13000000.0 108.0 2019-03-07T00:00:00 3 2.7 16.0 51.0 8 NaN ... 25.0 NaN Санкт-Петербург 18863.0 16028.0 1.0 482.0 2.0 755.0 NaN
1 7 3350000.0 40.4 2018-12-04T00:00:00 1 NaN 11.0 18.6 1 NaN ... 11.0 2.0 посёлок Шушары 12817.0 18603.0 0.0 NaN 0.0 NaN 81.0
2 10 5196000.0 56.0 2015-08-20T00:00:00 2 NaN 5.0 34.3 4 NaN ... 8.3 0.0 Санкт-Петербург 21741.0 13933.0 1.0 90.0 2.0 574.0 558.0

3 rows × 22 columns

count mean std min 25% 50% 75% max
total_images 23699.0 9.858475e+00 5.682529e+00 0.0 6.00 9.00 14.0 50.0
last_price 23699.0 6.541549e+06 1.088701e+07 12190.0 3400000.00 4650000.00 6800000.0 763000000.0
total_area 23699.0 6.034865e+01 3.565408e+01 12.0 40.00 52.00 69.9 900.0
rooms 23699.0 2.070636e+00 1.078405e+00 0.0 1.00 2.00 3.0 19.0
ceiling_height 14504.0 2.771499e+00 1.261056e+00 1.0 2.52 2.65 2.8 100.0
floors_total 23613.0 1.067382e+01 6.597173e+00 1.0 5.00 9.00 16.0 60.0
living_area 21796.0 3.445785e+01 2.203045e+01 2.0 18.60 30.00 42.3 409.7
floor 23699.0 5.892358e+00 4.885249e+00 1.0 2.00 4.00 8.0 33.0
kitchen_area 21421.0 1.056981e+01 5.905438e+00 1.3 7.00 9.10 12.0 112.0
balcony 12180.0 1.150082e+00 1.071300e+00 0.0 0.00 1.00 2.0 5.0
airports_nearest 18157.0 2.879367e+04 1.263088e+04 0.0 18585.00 26726.00 37273.0 84869.0
cityCenters_nearest 18180.0 1.419128e+04 8.608386e+03 181.0 9238.00 13098.50 16293.0 65968.0
parks_around3000 18181.0 6.114075e-01 8.020736e-01 0.0 0.00 0.00 1.0 3.0
parks_nearest 8079.0 4.908046e+02 3.423180e+02 1.0 288.00 455.00 612.0 3190.0
ponds_around3000 18181.0 7.702547e-01 9.383456e-01 0.0 0.00 1.00 1.0 3.0
ponds_nearest 9110.0 5.179809e+02 2.777206e+02 13.0 294.00 502.00 729.0 1344.0
days_exposition 20518.0 1.808886e+02 2.197280e+02 1.0 45.00 95.00 232.0 1580.0

Вывод¶

Есть пропуски в данных, но отрицательных значений нет. В датасете присутствуют значения различных типов, а также выбросы в данных

In [6]:
def get_empty_cols(data):
    '''Создание кортежа с столбцами, в которых присутствуют пропущенные значения'''
    cols_had_null = {col: round(data[col].isna().sum() / data.shape[0], 3) for col in data.columns}
    return {k: v for k, v in cols_had_null.items() if v}

def print_na_info(data):
    print("\n".join(["{:<25}{:<10.1%}".format(k, v) for k, v in get_empty_cols(data).items()]))

Определение и заполнение пропущенных значений¶

In [7]:
print_na_info(data)
ceiling_height           38.8%     
floors_total             0.4%      
living_area              8.0%      
is_apartment             88.3%     
kitchen_area             9.6%      
balcony                  48.6%     
locality_name            0.2%      
airports_nearest         23.4%     
cityCenters_nearest      23.3%     
parks_around3000         23.3%     
parks_nearest            65.9%     
ponds_around3000         23.3%     
ponds_nearest            61.6%     
days_exposition          13.4%     
In [8]:
data = data[~data['locality_name'].isna()]
In [9]:
def parse_locality(row):
    i = 0
    while i < len(row):
        if row[i].isupper():
            break
        i += 1
    return row[i:].lower().replace('ё', 'e').capitalize()

data.loc[:, 'locality_name'] = data.locality_name.apply(parse_locality)
In [10]:
locality_name_freq = data.locality_name.value_counts().to_frame()
pd.concat([locality_name_freq.head(), locality_name_freq.tail()])
Out[10]:
locality_name
Санкт-петербург 15721
Мурино 590
Кудрово 472
Шушары 440
Всеволожск 398
Пельгора 1
Каложицы 1
Платформа 69-й километр 1
Почап 1
Дзержинского 1
In [11]:
data.is_apartment = data.apply(lambda x: True if x['rooms'] > 1 and x['studio'] is False else False , axis=1)
"{:.1%}".format(data.is_apartment.isna().sum() / data.shape[0] * 100)
Out[11]:
'0.0%'
In [12]:
def insert_by_locality(data, col, fill_by_med=True):
    medians = data.groupby('locality_name')[col].median().dropna()
    for index, value in zip(medians.index, medians):
        data.loc[(data[col].isna()) & (data['locality_name'] == index), col] = value
    print('{:20} is filled by median of locality data'.format(col), end=' ')
    if data[col].isna().sum() and fill_by_med:
        data.loc[:, col] = data.fillna(data[col].median())
        print('& been filled by median of col')
    else:
        print()
    return data

def apply_insert_by_locality_for_cols(data, cols, fill_by_med=True):
    for col in cols:
        data = insert_by_locality(data, col, fill_by_med=fill_by_med) 
    return data
In [13]:
first_part = 'ceiling_height', 'floors_total', 'living_area', 'kitchen_area', 'balcony', 'days_exposition'
second_part = 'airports_nearest', 'cityCenters_nearest', 'parks_around3000', 'parks_nearest',\
                    'ponds_around3000', 'ponds_nearest' 

data = apply_insert_by_locality_for_cols(data, first_part)
data = apply_insert_by_locality_for_cols(data, second_part, fill_by_med=False)
print("#" * 100)
print_na_info(data)
ceiling_height       is filled by median of locality data & been filled by median of col
floors_total         is filled by median of locality data 
living_area          is filled by median of locality data & been filled by median of col
kitchen_area         is filled by median of locality data & been filled by median of col
balcony              is filled by median of locality data & been filled by median of col
days_exposition      is filled by median of locality data & been filled by median of col
airports_nearest     is filled by median of locality data 
cityCenters_nearest  is filled by median of locality data 
parks_around3000     is filled by median of locality data 
parks_nearest        is filled by median of locality data 
ponds_around3000     is filled by median of locality data 
ponds_nearest        is filled by median of locality data 
####################################################################################################
airports_nearest         20.4%     
cityCenters_nearest      20.4%     
parks_around3000         20.4%     
parks_nearest            25.4%     
ponds_around3000         20.4%     
ponds_nearest            20.9%     
In [14]:
def parse_two_cols(data, f, s):
    '''
        Нахождение данных в столбце s для столбца f DataFrame data
        Заполнение спец значением -1 и перевод к т д int
    '''
    def appl(row):
        if row[s] > 0 and f != 'cityCenters_nearest':
            return 3000
        else:
            return row[f]
        
    is_check = data[f].isna().sum() == ((data[f].isna()) & (data[s].isna())).sum()
    data[s] = data[s].fillna(-1.0).astype(int)    
    if is_check:
        print(f'There\'s no reason to search info in {s} for {f}')
    else:
        print(f'Search info in {s} for {f}')
        data[f] = data.apply(appl, axis=1)

    data[f] = data[f].fillna(-1.0).astype(int)

    if s != 'airports_nearest':
        data = data.drop(s, axis=1)
    return data

cols_parse = ('parks_around3000', 'parks_nearest'), ('ponds_around3000', 'ponds_nearest'),\
                ('airports_nearest', 'cityCenters_nearest')
cols_parse = tuple([item[::-1] for item in cols_parse])

for cols in cols_parse:
    data = parse_two_cols(data, *cols)
Search info in parks_around3000 for parks_nearest
Search info in ponds_around3000 for ponds_nearest
There's no reason to search info in airports_nearest for cityCenters_nearest
In [15]:
cols_parse = 'airports_nearest', 'cityCenters_nearest', 'parks_nearest', 'ponds_nearest'
data.loc[:, cols_parse].describe().T
Out[15]:
count mean std min 25% 50% 75% max
airports_nearest 23650.0 23512.449852 16667.539974 -1.0 11942.00 23140.0 35841.0 84869.0
cityCenters_nearest 23650.0 11511.521268 9633.272673 -1.0 3870.25 11753.0 15743.0 65968.0
parks_nearest 23650.0 1210.685666 1300.745383 -1.0 -1.00 460.0 3000.0 3190.0
ponds_nearest 23650.0 1425.453362 1330.468317 -1.0 503.00 503.0 3000.0 3000.0
In [16]:
for col in ['parks_nearest', 'ponds_nearest']:
    data[col] = data[col].astype(np.uint16)

Вывод¶

Были обработаны отсутствия значений в столбцах locality_name, apartmnent, ceiling_height, floors_total, living_area, kitchen_area, balcony, days_exposition, airports_nearest, cityCenters_nearest, parks_around3000, parks_nearest, ponds_around3000, ponds_nearest. (с помощью группировки по локации удалось заполнить пропуски в данных, но не полностью- оставшиеся пропуски были заполнены медианами по соответствующим столбцам) Также были обработаны значения столбца locality_name (выделены наименования)

Изменение типов данных и наименований столбцов¶

In [17]:
data["floors_total"] = data.floors_total.astype(np.uint8)
data["balcony"] = data.balcony.astype(np.uint8)
data['days_exposition'] = data.days_exposition.astype(np.uint32)
data['last_price'] = data.last_price.astype(np.uint32)
In [18]:
data.columns = [
    'total_images', 'price', 'total_area', 'first_day_exp',
    'rooms', 'ceiling_height', 'floors_total', 'living_area', 'floor',
    'apartment', 'studio', 'open_plan', 'kitchen_area', 'balcony',
    'location', 'air_nearest', 'city_nearest',
    'parks_nearest', 'ponds_nearest', 'days_exp'
]

Были изменены типы данных столбцов с тд float на int floors_total, balcony, days_exposition,last_price.

Изучение первичных параметров, удаление редких и выбивающихся значений.¶

In [19]:
def describe_enhanced(data, list_cols):
    '''
        Расширение базового метода `describe` нижниму и верхними оценками границ
        выбросов в данных (интерквартильный размах и 3 сигмы)
    '''
    descr = data[list_cols].describe().T
    descr["low_std"] = descr["mean"] - descr["std"] * 3
    descr["low_iqr"] = descr["25%"] - (descr["75%"] - descr["25%"]) * 1.5
    descr["up_iqr"] = descr["75%"] + (descr["75%"] - descr["25%"]) * 1.5
    descr["up_std"] = descr["mean"] + descr["std"] * 3
    if isinstance(descr, pd.Series):
        return descr.to_frame().T
    return descr

def del_anomal_values(data, info_descr, list_cols):
    for col in list_cols:
        low, up = info_descr.loc[col, 'low_iqr'], info_descr.loc[col, 'up_iqr'] 
        data = data[(low < data[col]) & (data[col] < up)]
    return data

Площадь недвижимости¶

В Санкт-Петербурге УН жилой площади также 9 кв. метров на человека в отдельных домах и квартирах, 15 кв. метров — для коммуналок.

In [20]:
col_info = ['kitchen_area', 'living_area', 'total_area'], ['Площадь кухни', 'Жилая площадь', 'Общая площадь'] 

areas_info = describe_enhanced(data, col_info[0])
display(areas_info)

fig = px.histogram(data, x=col_info[0],
                     barmode="overlay",
                     marginal="box",
                     range_x=(areas_info["min"].min(), areas_info["up_std"].max()),
                     title="Распределения " + ", ".join(map(lambda l: f"`{l}`", col_info[1])),
) 
fig.update_layout(
    xaxis_title='Площадь (кв м)',
    yaxis_title='Кол-во'
)
count mean std min 25% 50% 75% max low_std low_iqr up_iqr up_std
kitchen_area 23650.0 10.465152 5.631919 1.3 7.2 9.6 11.4575 112.0 -6.430605 0.81375 17.84375 27.360910
living_area 23650.0 34.054007 21.226308 2.0 19.0 30.4 41.1000 409.7 -29.624918 -14.15000 74.25000 97.732932
total_area 23650.0 60.329069 35.661808 12.0 40.0 52.0 69.7000 900.0 -46.656355 -4.55000 114.25000 167.314493
In [21]:
data = del_anomal_values(data, areas_info, col_info[0])

Были проанализированы площадь кухни, жилая и общая площади. Было выявлено, что имеют место аномалии (выбросы)
в данных. С помощью границы (Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных

Цена недвижимости¶

In [22]:
data.loc[:, 'price_m'] = data.price.apply(lambda x: x / 10 ** 6).astype(np.uint8)
In [23]:
col_info = 'price_m', 'Цена на момент снятия объявления, млн руб'

info = describe_enhanced(data, col_info[0])
display(info)

fig = px.histogram(data[col_info[0]],
                   barmode="overlay",
                   marginal="box",
                   title=col_info[1],
)
fig.update_layout(
    xaxis_title=col_info[1],
    yaxis_title="Кол-во"
)
count mean std min 25% 50% 75% max low_std low_iqr up_iqr up_std
price_m 21409.0 4.556402 2.953964 0.0 3.0 4.0 6.0 53.0 -4.305491 -1.5 10.5 13.418294
In [24]:
data = del_anomal_values(data, info, [col_info[0]])

Был добавлен столбец со значениями цены недвижимости в млн руб. Проанализировали цены на момент снятия объявления.С помощью границы (Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных

Количество комнат¶

In [25]:
col_info = 'rooms', 'Количество комнат'

info = describe_enhanced(data, col_info[0])
display(info)

fig = px.histogram(data[col_info[0]],
                   barmode="overlay",
                   marginal="box",
                   title=col_info[1],
                   nbins=30
)
fig.update_layout(
    xaxis_title=col_info[1],
    yaxis_title="Кол-во"
)
count mean std min 25% 50% 75% max low_std low_iqr up_iqr up_std
rooms 20561.0 1.893536 0.882271 0.0 1.0 2.0 3.0 6.0 -0.753277 -2.0 6.0 4.54035

Был проанализирован параметр - количество комнат, получилось выявить, что аномалии (выбросы) отсутствуют

Высота потолков¶

In [26]:
col_info = 'ceiling_height', 'Высота потолков (м)'

info = describe_enhanced(data, col_info[0])
display(info)

fig = px.histogram(data[col_info[0]],
                   barmode="overlay",
                   marginal="box",
                   title=col_info[1],
)
fig.update_layout(
    xaxis_title=col_info[1],
    yaxis_title="Кол-во"
)
count mean std min 25% 50% 75% max low_std low_iqr up_iqr up_std
ceiling_height 20561.0 2.699104 0.819571 1.0 2.55 2.7 2.7 32.0 0.24039 2.325 2.925 5.157818
In [27]:
data = del_anomal_values(data, info, [col_info[0]])
In [28]:
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data[col_info[0]],
                   barmode="overlay",
                   marginal="box",
                   title=col_info[1],
                   nbins=50
)
fig.update_layout(
    xaxis_title=col_info[1],
    yaxis_title="Кол-во"
)
count mean std min 25% 50% 75% max low_std low_iqr up_iqr up_std
ceiling_height 19024.0 2.633329 0.09716 2.34 2.55 2.65 2.7 2.92 2.34185 2.325 2.925 2.924808

Была проанализирована высота потолков (м), получилось выявить, что имеют место аномалии (выбросы)
в данных. С помощью границы (Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных

3. Добавление данных в таблицу¶

Цена квадратного метра¶

In [29]:
data.loc[:, 'price_sq_m'] = (data['price'] / data['total_area'])

День недели, месяц и год публикации объявления¶

In [30]:
data.loc[:, 'date_exp'] = pd.to_datetime(data['first_day_exp'])

data.loc[:, 'day_exp'] = data['date_exp'].dt.day
data.loc[:, 'month_exp'] = data['date_exp'].dt.month
data.loc[:, 'year_exp'] = data['date_exp'].dt.year

data = data.drop('first_day_exp', axis=1)

Этаж квартиры¶

In [31]:
def apl_floor(row):
    if row.floor == row.floors_total:
        return 'Последний'
    if row.floor == 1:
        return 'Первый'
    return 'Другой'

data.loc[:, 'floor'] = data.apply(apl_floor, axis=1)
data = data.drop('floors_total', axis=1)

Соотношение жилой и общей площади и отношение площади кухни к общей¶

In [32]:
data.loc[:, 'living_total'] = round(data.living_area / data.total_area, 3)
data.loc[:, 'kitchen_total'] = round(data.kitchen_area / data.total_area, 3)
In [33]:
data.loc[:, 'city_nearest_km'] = (data.city_nearest // 1000).astype(np.uint8)

Вывод¶

Были добавлены/изменены след данные: price_sq_m, date_exp, day_exp, month_exp, year_exp,
floor, living_total, kitchen_total, city_nearest_km

Описание данных после предобработки и добавления данных

  • balcony — число балконов
  • ceiling_height — высота потолков (м)
  • city_nearest — расстояние до центра города (м)
  • days_exp — сколько дней было размещено объявление (от публикации до снятия)
  • apartment — апартаменты (булев тип)
  • kitchen_area — площадь кухни в квадратных метрах (м²)
  • price — цена на момент снятия с публикации (руб)
  • living_area — жилая площадь в квадратных метрах(м²)
  • localation — название населённого пункта
  • open_plan — свободная планировка (булев тип)
  • parks_nearest — расстояние до ближайшего парка (м)
  • ponds_nearest — расстояние до ближайшего водоёма (м)
  • rooms — число комнат
  • studio — квартира-студия (булев тип)
  • total_area — площадь квартиры в квадратных метрах (м²)
  • total_images — число фотографий квартиры в объявлении
  • price_sq_m цена квадратного метра
  • date_exp дата создания объявления
  • day_exp день создания объявления
  • month_exp месяц создания объявления
  • year_exp год создания объявления
  • floor этаж (теперь признак категориальный)
  • living_total отношение жилой к общей площади
  • kitchen_total отношение площади кухни к общей
  • price_m — цена на момент снятия с публикации (млн. руб)
  • city_nearest_km — расстояние до центра города (м)
In [34]:
for col in ['total_images', 'rooms', 'ceiling_height', 'balcony', 'days_exp',\
            'day_exp', 'month_exp', 'year_exp', 'city_nearest_km']:
    data.loc[:, col] =  data.loc[:, col].astype(np.uint16)
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/2396148798.py:3: FutureWarning:

In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`

4. Исследовательский анализ данных¶

In [35]:
col_info = ['kitchen_total', 'living_total'], ['Пл-дь кухни к общей', 'Жилая пл-дь к общей']

info = describe_enhanced(data, col_info[0])
display(info)

fig = px.histogram(data, x=col_info[0],
                     barmode="overlay",
                     marginal="box",
                     range_x=(info["min"].min(), info["up_std"].max()),
                     title="Распределения " + ", ".join(map(lambda l: f"`{l}`", col_info[1])),
) 
fig.update_layout(
    xaxis_title='Значения отношений соответствующих площадей',
    yaxis_title='Кол-во'
)
count mean std min 25% 50% 75% max low_std low_iqr up_iqr up_std
kitchen_total 19024.0 0.191825 0.070288 0.044 0.138 0.179 0.237 0.825 -0.019040 -0.0105 0.3855 0.402691
living_total 19024.0 0.570148 0.118752 0.070 0.495 0.567 0.640 2.408 0.213893 0.2775 0.8575 0.926404
In [36]:
data = del_anomal_values(data, info, col_info[0])

Были проанализированы данные столбцов отношений площадей кухни к общей и жилой к общей площади соотв-но.
Было выявлено, что имеют место аномалии (выбросы) в данных. С помощью границы
(Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных

Изучение времени продажи квартиры.¶

In [37]:
col_info = "days_exp", "Время продажи квартиры"
In [38]:
info = describe_enhanced(data, col_info[0])
display(info)
fig = px.histogram(data[col_info[0]],
                   barmode="overlay",
                   marginal="box",
                   title=col_info[1],
                   nbins=50
)
fig.update_layout(
    xaxis_title=col_info[1],
    yaxis_title="Кол-во"
)
count mean std min 25% 50% 75% max low_std low_iqr up_iqr up_std
days_exp 18414.0 161.101173 196.08347 1.0 45.0 95.0 190.0 1580.0 -427.149236 -172.5 407.5 749.351582
In [39]:
data = del_anomal_values(data, info, [col_info[0]])

Были проанализированы данные столбца days_exp - количество дней доступа к объявлению о недвижимости.
Было выявлено, что имеют место аномалии (выбросы) в данных. С помощью границы
(Q1 - 1.5 * IQR < values < Q3 + 1.5 * IQR) были отброшены выбивающиеся значения из данных

Какие факторы больше всего влияют на стоимость квартиры?¶

In [40]:
col_parse = 'price_sq_m'
cols_parse = ['total_area', 'rooms', 'floor', 'city_nearest_km']

fig, axises = plt.subplots(1, len(cols_parse))  

for col, axis in zip(cols_parse, axises):
    if col != 'floor':
        correlation = round(data[col_parse].corr(data[col]), 2)
        print('{} ~ {: ^13} ->{: ^10}'.format(col_parse, col, correlation))  
    if col == 'floor':
        (data
            .groupby(col)[col_parse].median().reset_index()
            .plot(kind='bar', x='floor', y=col_parse, label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
        )
    elif col == 'rooms':
        (data
            .groupby(col)[col_parse].median().reset_index(drop=True)
            .plot(label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
        )
    else:
        (data
            .groupby(col)[col_parse].median().reset_index()
            .plot(kind='scatter', x=col, y=col_parse, label=col,\
                  legend=True, grid=True, alpha=0.5, figsize=(17, 7), ax=axis)
        )
   
        plt.xlabel(col)
plt.show()
price_sq_m ~  total_area   ->  -0.12   
price_sq_m ~     rooms     ->  -0.26   
price_sq_m ~ city_nearest_km ->  -0.65   
In [43]:
data_corr = data.corr()
data_corr_trunc = pd.DataFrame(np.tril(data_corr, k=-1),
                               columns=data_corr.columns,
                               index=data_corr.index
)
px.imshow(data_corr_trunc.replace({0: None}),
          text_auto=".1f",
          zmin=-1,
          zmax=1,
          width=700,
          height=700
)
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/1713752210.py:1: FutureWarning:

The default value of numeric_only in DataFrame.corr is deprecated. In a future version, it will default to False. Select only valid columns or specify the value of numeric_only to silence this warning.

In [44]:
col_parse = 'price_sq_m'
cols_parse = ['day_exp', 'month_exp', 'year_exp']

data_corr = data[cols_parse + [col_parse]].corr()
data_corr_trunc = pd.DataFrame(np.tril(data_corr, k=-1),
                               index=data_corr.index,
                               columns=data_corr.columns)
fig = px.imshow(data_corr_trunc.replace({0: None}),
          zmin=-1, zmax=1, text_auto=".1f")
fig.show()

px.line(data.groupby(col)[cols_parse].median(), log_y=True).show()
In [45]:
fig = px.line(data.groupby('city_nearest_km').agg(
    price_sq_m_median=("price_sq_m", np.median),
    price_sq_m_mean=("price_sq_m", np.mean),
))
fig.update_layout(
    title="Title",
    xaxis_title="x",
    yaxis_title="y",
)

Вывод¶

Больше всего на стоимость влияют след параметры: удалённость от города (км) и количество комнат
До ~70км зависимость не является линейной - это можно связать с множеством районов в пределах города, которые отличаются
по уровню и качеству застройки, после граничного значения км жилья +- одинаковое и видная прямая завимисимость между
удалённостью и ценой квадратного метра (обратная зависимость)
Меньшую корреляцию с ценой за кв метр имеет этаж (на первом этаже меньшее значение цены площади кв метра)
Наибольшую цену кв метра, имеют объявления, созданные в 27-28 числах августа. Также наблюдается тенденция в росте
цены кв метра с 2016 по 2019 год.

In [46]:
data_top_10_count = (data
    .groupby('location').agg({'rooms':'count', 'price_sq_m':'mean'}).reset_index()
    .sort_values(by='rooms', ascending=False).reset_index(drop=True)
)
data_top_10_count.loc[:, 'price_sq_m'] = data_top_10_count['price_sq_m'].astype(int)
data_top_10_count.columns = ['location', 'count', 'price_sq_m']

print('{:#^70}'.format('Первые 10 нас. пунктов по количеству объявлений'))
display(data_top_10_count.head(10))
###########Первые 10 нас. пунктов по количеству объявлений############
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/1076292836.py:5: FutureWarning:

In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`

location count price_sq_m
0 Санкт-петербург 10303 103944
1 Мурино 458 85616
2 Шушары 378 77880
3 Кудрово 343 95485
4 Всеволожск 319 66775
5 Колпино 285 75232
6 Парголово 267 90432
7 Пушкин 260 99324
8 Гатчина 244 68386
9 Выборг 174 57208
In [47]:
data_top_price = (data
#                     .groupby('location')['price_sq_m'].mean().reset_index()
            .groupby('location').agg({'rooms':'count', 'price_sq_m':'mean'}).reset_index()
            .sort_values(by='price_sq_m', ascending=False)
)
data_top_price.loc[:, 'price_sq_m'] = data_top_price['price_sq_m'].astype(int)
data_top_price.columns = ['location', 'count', 'price_sq_m']

display('{:#^70}'.format('Топ по цене (первые 10)'))
display(data_top_price.head(10).reset_index(drop=True))
display('{:#^70}'.format('Топ по цене (последние 10)'))
display(data_top_price.tail(10).reset_index(drop=True))
/var/folders/ks/pthjmky532s9spw_v5gndt6wb6v_26/T/ipykernel_72557/965368589.py:6: FutureWarning:

In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`

'#######################Топ по цене (первые 10)########################'
location count price_sq_m
0 Лисий нос 2 113728
1 Санкт-петербург 10303 103944
2 Сестрорецк 113 102439
3 Зеленогорск 20 100123
4 Пушкин 260 99324
5 Мистолово 8 97145
6 Левашово 1 96997
7 Кудрово 343 95485
8 Парголово 267 90432
9 Стрельна 35 88627
'######################Топ по цене (последние 10)######################'
location count price_sq_m
0 Житково 1 14264
1 Ефимовский 2 14149
2 Ям-тесово 2 13711
3 Сижно 1 13709
4 Тeсово-4 1 12931
5 Совхозный 2 12629
6 Выскатка 2 12335
7 Вахнова кара 1 11688
8 Свирь 2 11481
9 Старополье 3 11206

Вывод¶

Были найдены первые 10 нас пунктов по кол-ву объявлений (Петербург, Мурино, Шушары).
Также были обнаружены первые и последние 10 нас пунктов по цене квадратного метра (см 2 таблицы выше).

Изучение предложений квартир¶

In [48]:
city_nearest_info = describe_enhanced(data, ['city_nearest_km'])
city_nearest_info.T
Out[48]:
city_nearest_km
count 16595.000000
mean 71.118530
std 101.371557
min 0.000000
25% 12.000000
50% 15.000000
75% 35.000000
max 255.000000
low_std -232.996142
low_iqr -22.500000
up_iqr 69.500000
up_std 375.233201
In [49]:
ax = (data
    .query('location == "Санкт-Петербург"')
    .groupby('city_nearest_km')['price_sq_m'].mean()
    .plot(label='Цена кв метра в Петербурге', figsize=(15, 7), grid=True, legend=True)
)

plt.xlabel('Удалённость от центра Петербурга')
plt.ylabel('Цена кв метра недвижимости')
plt.show()
In [51]:
border_center = 7

Выделиние сегмента квартир в центре и его анализ.¶

Вывод¶

Посчитали среднюю цену для каждого километра и постройте график. Чем больше удаляемся от центра, тем цена ниже
(За исключением пары мест на 42 и 59 км - может быть связано с элитными пригородскими коттеджными посёлками,
в которых цены за кв метр существенно выше). Определили границу центральной зоны Петербурга - по графику смогли определить
значение (примерно 7-ой км является границей этой зоны, имеющей форму приблизительно окружности)

In [52]:
# cc - city_center
list_cols = 'location', 'total_area', 'price_m', 'rooms', 'ceiling_height', 'price_sq_m', 'city_nearest_km', 'floor',\
                'day_exp', 'month_exp', 'year_exp'
data_cc = data.query('0 <= city_nearest_km <= @border_center').loc[:, list_cols]
data_cc.describe().T
Out[52]:
count mean std min 25% 50% 75% max
total_area 1104.0 58.546685 18.641605 17.0 43.975000 56.900000 71.00000 114.200000
price_m 1104.0 6.206522 1.928672 1.0 5.000000 6.000000 8.00000 10.000000
rooms 1104.0 2.092391 0.889434 0.0 1.000000 2.000000 3.00000 5.000000
ceiling_height 1104.0 2.000000 0.000000 2.0 2.000000 2.000000 2.00000 2.000000
price_sq_m 1104.0 118871.933953 28670.807152 26250.0 98585.239033 114557.844492 136043.48238 262711.864407
city_nearest_km 1104.0 4.681159 1.696823 0.0 4.000000 5.000000 6.00000 7.000000
day_exp 1104.0 15.083333 8.600446 1.0 8.000000 15.000000 23.00000 31.000000
month_exp 1104.0 6.664855 3.399109 1.0 4.000000 7.000000 10.00000 12.000000
year_exp 1104.0 2017.352355 0.892161 2015.0 2017.000000 2017.000000 2018.00000 2019.000000
In [ ]:
col_parse = 'price_sq_m'
cols_parse = ['total_area', 'rooms', 'floor', 'city_nearest_km']

fig, axises = plt.subplots(1, len(cols_parse))  

for col, axis in zip(cols_parse, axises):
    if col != 'floor':
        correlation = round(data_cc[col_parse].corr(data_cc[col]), 2)
        print('{} ~ {: ^13} ->{: ^10}'.format(col_parse, col, correlation))  
    if col == 'floor':
        (data_cc
            .groupby(col)[col_parse].median().reset_index()
            .plot(kind='bar', x='floor', y=col_parse, label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
        )
    elif col == 'rooms':
        (data_cc
            .groupby(col)[col_parse].median().reset_index(drop=True)
            .plot(label=col, legend=True, grid=True, figsize=(17, 7), ax=axis)
        )
    else:
        (data_cc
            .groupby(col)[col_parse].median().reset_index()
            .plot(kind='scatter', x=col, y=col_parse, label=col,\
                  legend=True, grid=True, alpha=0.7, figsize=(17, 7), ax=axis)
        )
   
        plt.xlabel(col)
plt.show()
price_sq_m ~  total_area   ->  -0.46   
price_sq_m ~     rooms     ->  -0.52   
price_sq_m ~ city_nearest_km ->   0.04   
In [ ]:
col_parse = 'price_sq_m'
cols_parse = ['day_exp', 'month_exp', 'year_exp']

fig, axises = plt.subplots(1, len(cols_parse))

for col, axis in zip(cols_parse, axises):
    correlation = round(data_cc[col_parse].corr(data_cc[col]), 2)
    print('{} ~ {: ^13} ->{: ^10}'.format(col_parse, col, correlation))
        
    (data_cc
        .groupby(col)[col_parse].median()
        .plot(label='Цена кв метра', legend=True, grid=True, figsize=(17, 7), ax=axis)
    )
plt.show()
price_sq_m ~    day_exp    ->   0.03   
price_sq_m ~   month_exp   ->  -0.05   
price_sq_m ~   year_exp    ->   0.18   

Вывод¶

Больше всего на стоимость квадратного метра в центре города влияют след параметры:
количество комнат (превалирует вариант жилья с одной комнатой) и площадь (в отличие от всей области в целом)
Также отличаются от ситуации по области и зависимость цены от времени продажи - 30-ые числа для старта объявления
оказываются наиболее удачными в плане продажи жилья и первой четверти года (март месяц). Что же касаемо тенденции
в росте цены кв метра, то можно утверждать, что цены на кв метр в центре не так подвластны снижению.
Рост наблюдается с 2015 по 2019 год. (нет "обвала" цен в 15 году, как наблюдается для всеё области в целом)

In [ ]: